Stack too Deep を攻略する
目的
EVMで多くのデータを扱ったりすると遭遇するStack too deepエラーの原因と解決策を考えます。
この記事はこんな人にぴったりです
stack too deepが起きる原因を知りたい
functionの引数にたくさん値を入れたいけどstack too deepになる
eventの引数にたくさん値を入れたいけどstack too deepになる
大量のデータをアトミックに作成、更新したいけどstack too deepになる
stck too deepってよく聞くけどいまいちわからないから何なのか知りたい
Stack too deepはなぜ起きるのか
Stack too deep とは関数の引数が一定数を越えた場合 ”など” に見られるエラー
https://gyazo.com/13ab37f8330f43a9cdd0f379b29ff0c4
例えばこんな感じ
https://gyazo.com/2dcc60b3975f732a46ac26b2b217c306
しかし、stack too deepはシンプルに引数の数だけに起因するものではありません。
なぜこんなエラーが起きるのか?
そもそもEthereum Virtual Machineにはdataを保存できる場所はstorage、memory、stackの3つがあります。
https://gyazo.com/ddd238f285aa6b22316f8b6535ac184a
EVMの全体像はこんな感じ
https://gyazo.com/0df230b39e87828e9c64cc9f3c7c6e34
今回のエラーと関係があるのはStack
https://gyazo.com/cd978dd0a7ddce68fc16ff9528a9d9bf
stack: A stack is a data type which serves as a collection of elements, with two principal operations: push which adds an element to the top of the stack, and pop which removes the topmost element from the stack.
上図のようにEVMのStackは256ビットのelementを1024個持っており、operationsとやりとりできるのはstackの上から16個のelementまでに限られています。したがって、16番目より下のelementを参照する処理を実行しようとするとリーチできる"深さ"を超えることになり"stack too deep"エラーとなる、というからくりです。ではどうすればstack too deep エラーを回避できるでしょうか。
Stackの中を覗く
Deconstructing a Solidity Contract(Part 1~6)👈長いけど全体を細かく切り分けてきっちり解説
Ethereum Virtual Machine Opcodes👈opcodeの図解が良い
などがかなり良質な資料となっています。特に以下を読み進めるにあたってopcodeの処理を図でイメージすることが重要なので2番目の図解の資料は必ず目を通してください。
opcodeはsolidityのコードをEVMが読めるようにコンパイルしたものであるため当然solidityのコードとEVMのオペコードには対応関係があります。例えばとあるコントラクトにbalanceOfという関数があるとしましょう。
silidityのコードは以下のようであるとき
function balanceOf(address _owner) public view returns (uint256) {
return balances[_owner];
}
これに対応するオペコードは以下のようになります。
https://gyazo.com/8f77cef43d6c66a27c30741194af01c0
まぁ、なんとなく仕組みがわかっていただけたでしょうか。要するにsolidityやvyperは人間読みやすいように作るられ他コード、opcodeはEVMが読みやすいように作られたコードです。opcodeはsolidityやvyperなどの人間読みやすいコードをコンパイルすることで生成されます。
アヤシイ処理を探す
なんとなくopcodeについてわかったと思いますが、実際にどのopcodeが原因でstack too deepを引き起こしているのでしょうか。stack too deepの原因はコントラクトによって違うはずですがstack too deepにを引き起こす可能性があるオペコードを見つけることはできそうです。
“Stack Too Deep”- Error in Solidity
👆の記事では引数が16個ある関数内でイベントを発火しようとする際に、先頭の引数をeventとして吐こうとするとstack too deepになることを取り上げており、原因はDUPn (n番目のelementを複製するopcode)というopcodeがn>=17になる時にあると突き詰めています。詳細は記事を読んでいただくことにして、ここで重要なのはDUPn (n番目のelementを複製するopcode)のnが17になってしまうことがstack too deepの原因だったということです。
https://gyazo.com/9dfc014e72c826cf806facef1edbe8a2
Stackは16番目のスロットまでしか操作できないのでDUP17で17番目の値を複製しようとしても参照できる範囲(16番目まで)を超えてしまって取得できないということです。
ここからわかることは DUP を使用するプログラムは注意が必要ということです。
Ethereum Virtual Machine Opcodesをもう一度見てみましょう
DUPと同様にn番目のelementを参照する処理はもう1つあります。SWAPnです。
SWAPn (1番目とn番目のelementを入れ替えるopcode)もn=17となる処理を実行しようとするとstack too deepを引き起こすことが予測できます。逆にいうと他のopcodeは理論上stack too deepを引き起こす "直接的な" 原因にはならないと予想できます。
モジュールを分析する
よし、じゃあDUPとSWAPを使うときだけ気をつけて実装しましょう!
DUPnやSWAPnのnが大きくなる処理にはfunction、Eventなどがあります。これはfunction、Eventの引数がそれぞれstackのスロットを1つずつ使うからです。
function
引数は16個が限度
event
引数は14個が限度
struct
プロパティは無限にとれる。
publicな配列や変数に格納する際はgetter functionが生成されるので関数の制約が生まれ、プロパティは12個が限界に。
問題を整理する
functionの引数にたくさん値を入れたいけどstack too deepになる
eventの引数にたくさん値を入れたいけどstack too deepになる
大量のデータをアトミックに作成、更新したいけどstack too deepになる
解決案
functionの引数にたくさん値を入れたいけどstack too deepになる
functionの引数の限度は16個なのでそれ以上の数字を扱いたいのであれば関数を分割するのが自然であろう。もしどうしても引数に大量の数字を入れたければstructを事前にいくつか作っておいてそれらを引数に入れるということも可能ではある。
eventの引数にたくさん値を入れたいけどstack too deepになる
これもfunction同様structを作ってそれをeventではくというのがベストプラクティス
大量のデータをアトミックに作成、更新したいけどstack too deepになる
これを上記の解決先に工夫を加えることで可能になる。手順は以下の通り。
1. まず必要な情報をすべて盛り込んだstructを定義する
2. 次に関数1で空のstructを作成する
3. 次に関数2~nでstructの中身を更新していく
4. 最後のn番目の関数を実行してstructが完成した時にeventでstructをまるっと投げればアトミックに大量のデータを作成、更新することができる。
参考資料
番外編
EVMはSTDを解決できてもさらなる罠があります。例えば
1. gasコスト
2. コントラクトサイズ
nrryuya.icon > EVMに雑に書いてた tomoaki.icon>パイセンあざす!